module AxiReadOnlyOnChipRom (
  input  logic             io_axi_ar_valid,
  output logic             io_axi_ar_ready,
  input  logic    [31:0]   io_axi_ar_payload_addr,
  input  logic    [4:0]    io_axi_ar_payload_id,
  input  logic    [7:0]    io_axi_ar_payload_len,
  input  logic    [2:0]    io_axi_ar_payload_size,
  input  logic    [1:0]    io_axi_ar_payload_burst,
  
  output logic             io_axi_r_valid,
  input  logic             io_axi_r_ready,
  output logic    [127:0]  io_axi_r_payload_data,
  output logic    [4:0]    io_axi_r_payload_id,
  output logic             io_axi_r_payload_last,
  
  input  logic             clk,
  input  logic             reset
);

localparam int RomSize = 895;

const logic [RomSize-1:0][127:0] mem = {
        128'h00000000_00000000_00000000_00000000,
        128'h0a0d2165_6e6f6420_0a0d2165_6e6f6420,
        128'h00000000_00206567_0a0d2165_6e6f6420,
        128'h616d6920_746f6f62_0a0d2165_6e6f6420,
        128'h20676e69_79706f63_0a0d2165_6e6f6420
};

localparam AXI_DATA_WIDTH = 128;
localparam LOG_NR_BYTES = $clog2(AXI_DATA_WIDTH/8);

localparam AXI_ADDR_WIDTH = 32;
localparam AXI_ID_WIDTH = 5;

typedef enum logic [1:0] { FIXED = 2'b00, INCR = 2'b01, WRAP = 2'b10 } axi_burst_t;
typedef struct packed {
    logic [AXI_ID_WIDTH-1:0]   id;
    logic [AXI_ADDR_WIDTH-1:0] addr;
    logic [7:0]                len;
    logic [2:0]                size;
    axi_burst_t                burst;
} ax_req_t;


logic [4:0]  payload_id;

logic req_grant;
logic [AXI_ADDR_WIDTH-1:0]   addr;
logic [AXI_DATA_WIDTH-1:0]   payload_data;


function automatic logic [AXI_ADDR_WIDTH-1:0] get_wrap_boundary (input logic [AXI_ADDR_WIDTH-1:0] unaligned_address, input logic [7:0] len);
    logic [AXI_ADDR_WIDTH-1:0] warp_address = '0;
    //  for wrapping transfers ax_len can only be of size 1, 3, 7 or 15
    if (len == 4'b1)
        warp_address[AXI_ADDR_WIDTH-1:1+LOG_NR_BYTES] = unaligned_address[AXI_ADDR_WIDTH-1:1+LOG_NR_BYTES];
    else if (len == 4'b11)
        warp_address[AXI_ADDR_WIDTH-1:2+LOG_NR_BYTES] = unaligned_address[AXI_ADDR_WIDTH-1:2+LOG_NR_BYTES];
    else if (len == 4'b111)
        warp_address[AXI_ADDR_WIDTH-1:3+LOG_NR_BYTES] = unaligned_address[AXI_ADDR_WIDTH-3:2+LOG_NR_BYTES];
    else if (len == 4'b1111)
        warp_address[AXI_ADDR_WIDTH-1:4+LOG_NR_BYTES] = unaligned_address[AXI_ADDR_WIDTH-3:4+LOG_NR_BYTES];
    return warp_address;
endfunction


logic [2:0] req_state;

enum logic [2:0] { IDLE, READ, RESP }  req_state_d, req_state_q;

logic [AXI_ADDR_WIDTH-1:0] aligned_address;
logic [AXI_ADDR_WIDTH-1:0] wrap_boundary;
logic [AXI_ADDR_WIDTH-1:0] upper_wrap_boundary;
logic [AXI_ADDR_WIDTH-1:0] cons_addr;

ax_req_t                   ax_req_d, ax_req_q;

logic [AXI_ADDR_WIDTH-1:0] req_addr_d, req_addr_q;
logic [7:0]                cnt_d, cnt_q;

always_comb begin
  // address generation
  aligned_address = {io_axi_ar_payload_addr[AXI_ADDR_WIDTH-1:LOG_NR_BYTES], {{LOG_NR_BYTES}{1'b0}}};
  wrap_boundary = get_wrap_boundary(io_axi_ar_payload_addr, io_axi_ar_payload_len);
  // this will overflow
  upper_wrap_boundary = wrap_boundary + ((io_axi_ar_payload_len + 1) << LOG_NR_BYTES);
  // calculate consecutive address
  cons_addr = aligned_address + (cnt_q << LOG_NR_BYTES);
  
  req_state_d = req_state_q;
  req_addr_d = req_addr_q;
  cnt_d      = cnt_q;

  req_grant = 1'b0;
  addr = '0;

  case (req_state_q)
    IDLE : begin
      if (io_axi_ar_valid) begin
        io_axi_ar_ready = 1'b1;
        ax_req_d       = {io_axi_ar_payload_id, 
                          io_axi_ar_payload_addr,
                          io_axi_ar_payload_len,
                          io_axi_ar_payload_size,
                          io_axi_ar_payload_burst
                          };

        req_grant = 1'b1;
        addr = io_axi_ar_payload_addr;
        req_addr_d = io_axi_ar_payload_addr;
        cnt_d          = 1;
        req_state_d = READ;
      end
    end 

    READ : begin
      req_grant = 1'b1;
      addr = req_addr_q;

      io_axi_r_valid = 1'b1;
      io_axi_r_payload_data = payload_data;
      io_axi_r_payload_id = ax_req_q.id;
      io_axi_r_payload_last = (cnt_q == ax_req_q.len + 1);

      if(io_axi_r_ready) begin
        // ----------------------------
        // Next address generation
        // ----------------------------
        // handle the correct burst type
        case (ax_req_q.burst)
              FIXED, INCR: addr = cons_addr;
              WRAP:  begin
                  // check if the address reached warp boundary
                  if (cons_addr == upper_wrap_boundary) begin
                      addr = wrap_boundary;
                  // address warped beyond boundary
                  end else if (cons_addr > upper_wrap_boundary) begin
                      addr = ax_req_q.addr + ((cnt_q - ax_req_q.len) << LOG_NR_BYTES);
                  // we are still in the incremental regime
                  end else begin
                      addr = cons_addr;
                  end
              end
        endcase

        if (io_axi_r_payload_last) begin
            req_state_d = IDLE;
            // we already got everything
            req_grant = 1'b0;
        end
        req_addr_d = addr;
        cnt_d = cnt_q + 1;
      end
    end

  endcase

end

always_ff @(posedge clk) begin
    if (reset) begin
        req_state_q    <= IDLE;
        ax_req_q  <= '0;
        req_addr_q <= '0;
        cnt_q      <= '0;
    end else begin
        req_state_q    <= req_state_q;
        ax_req_q   <= ax_req_d;
        req_addr_q <= req_addr_d;
        cnt_q      <= cnt_d;
    end
end


logic [$clog2(RomSize)-1:0] addr_q;
always_ff @(posedge clk) begin
  if (req_grant) begin
    addr_q <= io_axi_ar_payload_addr[$clog2(RomSize)-1+3:4];
  end
end

assign payload_data = (addr < RomSize) ? mem[addr[$clog2(RomSize)-1+3:4]] : '0;


endmodule